W04-1. Dockerfile 과 이미지 빌드 - 강의 노트
4주차 강의 노트: Dockerfile 과 이미지 빌드 · 배포
- Dockerfile 의 기본 명령어(FROM, WORKDIR, COPY, RUN, ENV, ARG, USER, EXPOSE, CMD)를 읽고 쓸 수 있다.
- 레이어 캐시를 이해하고, 빌드가 빠른 순서로 Dockerfile 을 쓸 수 있다.
- 내 이미지를 Docker Hub 에 올리고(push), 남의 이미지를 받아(pull) 실행할 수 있다.
- Mac(arm64)과 Windows(amd64)에서 모두 도는 멀티 플랫폼 이미지를 만들 수 있다.
진행: 이론 15분 (이 노트) → 실습 45분 (실습 가이드)
- 포트
-p, 바인드 마운트 / 볼륨-v, 사용자 정의 네트워크--network - "컨테이너는 버리고 새로 만들 수 있고, 데이터는 볼륨에 둔다"
1. 이미지는 어떻게 만들어지나
지금까지는 남이 만든 이미지(nginx, postgres)를 썼습니다. 오늘은 스터디 예제 앱 "방명록(guestbook)" 을 내 이미지로 만듭니다.
flowchart TB
S["📄 소스 코드
app.py, requirements.txt"] --> F["📝 Dockerfile
(만드는 순서를 적은 레시피)"]
F -- "docker build" --> I["📦 이미지
guestbook:v1"]
I -- "docker push" --> H["☁️ Docker Hub
내아이디/guestbook:v1"]
H -- "docker pull / run" --> O["옆 사람 노트북
서버, Kubernetes"]| 단계 | 명령 | 2주차에 본 것과 연결 |
|---|---|---|
| 레시피 작성 | Dockerfile | docker image history 의 CREATED BY 한 줄 한 줄이 Dockerfile 명령이었습니다 |
| 빌드 | docker build -t 이름:태그 . |
레시피대로 레이어를 하나씩 쌓아 이미지를 만듭니다 |
| 배포 | docker push |
레지스트리(Docker Hub)에 올립니다 |
| 실행 | docker run |
어디서든 같은 이미지로 실행합니다 |
2. 오늘 만드는 앱: 방명록(guestbook)
이 앱 하나를 10주차까지 계속 씁니다.
| 기능 | 이 스터디에서 쓰는 곳 |
|---|---|
| 이름과 한마디를 남기는 웹 페이지 (Python Flask) | 4주차: 첫 이미지 |
DATABASE_URL 이 없으면 메모리, 있으면 PostgreSQL 에 저장 |
5주차 Compose, 9주차 K8s |
| 화면에 HOSTNAME(응답한 컨테이너 이름) 표시 | 7~8주차 로드밸런싱 확인 |
APP_VERSION, APP_COLOR 로 버전과 색 표시 |
7주차 롤링 업데이트, 10주차 카나리 배포 |
/healthz, /readyz 상태 확인 주소 |
10주차 헬스체크(Probe) |
우리는 개발자가 아니라 이 앱을 컨테이너로 만들고 운영하는 사람의 입장입니다. Python 코드는 복사해서 쓰고, Dockerfile 에 집중합니다.
처음에는 웹 서버(gunicorn)를 프로세스 2개로 띄웠습니다. 그랬더니 프로세스마다 메모리가 따로라서, 새로고침할 때마다 글이 보였다 안 보였다 했습니다.
그래서 메모리 모드에서는 프로세스 1개 + 스레드 4개로 띄웁니다. "데이터를 프로세스 메모리에 두면 안 된다" 는 것을 보여 주는 좋은 예입니다. 그래서 5주차에 DB 를 붙입니다.
3. Dockerfile 한 줄씩 읽기
# ① 무엇에서 시작할지 (베이스 이미지)
FROM python:3.14-slim
# ② 작업 폴더
WORKDIR /app
# ③ 라이브러리 목록 복사 → ④ 라이브러리 설치
COPY requirements.txt .
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
# ⑤ 소스 코드 복사
COPY app.py .
# ⑥ 일반 사용자를 만들고, 이후는 이 사용자로 실행
RUN useradd --create-home appuser
USER appuser
# ⑦ 빌드할 때 바꿀 수 있는 값(ARG) → 실행할 때 쓰는 환경 변수(ENV)로 넘김
ARG APP_VERSION=v1
ARG APP_COLOR=royalblue
ENV APP_VERSION=${APP_VERSION} \
APP_COLOR=${APP_COLOR}
# ⑧ 이 앱의 포트 (안내문)
EXPOSE 8000
# ⑨ 메인 프로세스
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "4", "app:app"]
# 주석은 줄 맨 앞에 있을 때만 주석입니다. COPY app.py . # 설명 처럼 명령 뒤에 붙이면 # 과 설명 까지 명령의 일부(복사할 파일 이름) 로 처리되어 빌드가 실패합니다.
| 명령 | 하는 일 | 레이어가 생기나? |
|---|---|---|
FROM |
시작할 이미지. 공식 이미지에서 시작하는 것이 기본 | (베이스의 레이어를 그대로 씀) |
WORKDIR |
이후 명령이 실행될 폴더 (없으면 만듦) | ✅ |
COPY 원본 대상 |
내 폴더의 파일을 이미지 안으로 복사 | ✅ |
RUN 명령 |
이미지를 만드는 중에 명령 실행 (설치 등) | ✅ |
ENV 이름=값 |
컨테이너 안의 환경 변수 (실행할 때 -e 로 덮어쓸 수 있음) |
설정만 |
ARG 이름=기본값 |
빌드할 때만 쓰는 변수 (--build-arg 로 바꿈) |
설정만 |
USER |
이후 명령과 컨테이너를 실행할 사용자 | 설정만 |
EXPOSE |
쓰는 포트를 문서로 남김 (실제로 여는 것은 -p) |
설정만 |
CMD |
컨테이너가 시작될 때 실행할 메인 프로세스 | 설정만 |
RUN 과 CMD 를 헷갈리지 마세요
RUN은 이미지를 만들 때 한 번 실행됩니다 (예: 프로그램 설치).CMD는 컨테이너를 켤 때마다 실행됩니다 (예: 웹 서버 시작).
이렇게 쓰면 앱이 PID 1 이 되어, docker stop 이 보내는 신호(SIGTERM)를 앱이 직접 받습니다. 2주차에 본 "정중한 종료" 가 제대로 동작합니다.
왜 slim 베이스 이미지인가
| 태그 | 특징 |
|---|---|
python:3.14 |
빌드 도구까지 다 들어 있어 큼 |
python:3.14-slim |
실행에 필요한 것만 남긴 Debian 기반. 대부분 이것으로 충분 |
python:3.14-alpine |
가장 작지만, 일부 라이브러리 설치가 까다로울 수 있음 |
왜 USER appuser 인가 (보안 기본기)
컨테이너 안의 기본 사용자는 root(관리자) 입니다. 앱에 보안 구멍이 생겨 공격당하면 공격자가 컨테이너 안의 root 권한을 얻습니다. 일반 사용자로 실행하면 피해를 줄일 수 있습니다. 실무에서 기본으로 요구하는 항목입니다.
4. 레이어 캐시: 빌드가 빨라지는 원리
Dockerfile 의 명령 하나하나가 레이어(층) 가 됩니다. Docker 는 이전에 같은 명령으로 같은 결과를 만든 적이 있으면 그 레이어를 다시 만들지 않고 재사용(CACHED) 합니다.
규칙: 어떤 층이 바뀌면, 그 층과 그 위의 모든 층을 다시 만듭니다.
flowchart TB
subgraph G["✅ 좋은 순서 (우리 Dockerfile)"]
direction TB
G1["FROM python:3.14-slim"] --> G2["COPY requirements.txt"]
G2 --> G3["RUN pip install ← 무거움 (약 4~5초)"]
G3 --> G4["COPY app.py ← 코드 수정 시 여기부터만"]
end
subgraph B["❌ 나쁜 순서"]
direction TB
B1["FROM python:3.14-slim"] --> B2["COPY . . ← 코드 수정 시 여기부터"]
B2 --> B3["RUN pip install ← 매번 다시 설치!"]
end| 코드(app.py)만 고치고 다시 빌드했을 때 | 다시 하는 일 | 실측 시간 |
|---|---|---|
| ✅ 좋은 순서 | COPY app.py 이후만 |
약 1.6초 |
| ❌ 나쁜 순서 | COPY . . 이후 전부 (pip install 다시) |
약 6.7초 |
잘 안 바뀌는 것은 위에, 자주 바뀌는 것은 아래에.
라이브러리 목록(requirements.txt, package.json 등)을 먼저 복사해서 설치하고, 소스 코드는 맨 나중에 복사합니다.
우리 앱은 작아서 차이가 몇 초지만, 실무 앱은 몇 분 대 몇 초 차이가 납니다.
.dockerignore
docker build -t ... . 의 마지막 . 은 "이 폴더를 빌드 재료(빌드 컨텍스트)로 쓴다" 는 뜻입니다. 이미지에 들어가면 안 되는 파일(캐시, 비밀번호 파일, .git 등)은 .dockerignore 에 적어서 제외합니다. .gitignore 와 같은 방식입니다.
5. 이미지 이름 짓기와 배포
5-1. 태그 전략
| 태그 | 의미 |
|---|---|
guestbook:v1, guestbook:v2 |
버전마다 다른 태그. 운영에서는 항상 버전 태그를 씁니다 |
guestbook:latest |
"마지막에 올린 것" 이라는 이름표일 뿐. 운영에서는 피합니다 |
5-2. Docker Hub 에 올리기
Docker Hub 에 올리려면 이미지 이름이 내아이디/저장소:태그 형식이어야 합니다.
guestbook:v1 ← 내 노트북에서만 쓰는 이름
내아이디/guestbook:v1 ← Docker Hub 에 올릴 수 있는 이름
docker.io/내아이디/guestbook:v1 ← 생략 없는 전체 이름 (2주차)
누구나 받을 수 있습니다. 회사 코드, 비밀번호, 설정 파일이 들어간 이미지는 절대 공개 저장소에 올리지 마세요.
실무에서는 회사나 클라우드의 비공개 레지스트리(AWS ECR, NCR, GitHub Container Registry 등)를 씁니다.
5-3. 멀티 플랫폼 이미지: Mac 과 Windows 가 섞인 팀의 필수 지식
| 내 노트북 | docker build 로 만든 이미지의 CPU |
문제 |
|---|---|---|
| Windows / Intel Mac | amd64 |
Apple Silicon Mac 에서는 에뮬레이션으로 느리게 돌거나 안 될 수 있음 |
| Apple Silicon Mac | arm64 |
대부분의 클라우드 서버(amd64)에서 exec format error 로 실행 안 됨 |
해결: 두 CPU 용을 한 번에 만들어 같은 태그로 올립니다(2주차에 본 nginx 처럼).
docker buildx build --platform linux/amd64,linux/arm64 -t 내아이디/guestbook:v1 --push .
buildx는 Docker 의 확장 빌드 도구입니다. Docker Desktop 에 기본으로 들어 있습니다.- 내 CPU 가 아닌 쪽은 Docker Desktop 이 에뮬레이션(흉내 내기)으로 빌드합니다. 그래서 시간이 더 걸립니다(실측 약 45초).
--push를 붙이면 빌드가 끝나자마자 Docker Hub 에 올립니다.
그래서 오늘 내아이디/guestbook:v1, 내아이디/guestbook:v2 두 개를 꼭 Docker Hub 에 올려 둡니다.
핵심 요약
4주차 핵심
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
① Dockerfile FROM → WORKDIR → COPY(목록) → RUN(설치) → COPY(코드)
→ USER(일반 사용자) → ENV/ARG → EXPOSE → CMD(메인 프로세스)
② RUN vs CMD RUN = 빌드할 때 1번 / CMD = 컨테이너 켤 때마다
③ ARG vs ENV ARG = 빌드할 때만 (--build-arg) / ENV = 실행할 때도 (-e 로 덮어씀)
④ 캐시 바뀐 층부터 위로 전부 다시 → 안 바뀌는 것은 위, 자주 바뀌는 것은 아래
⑤ 배포 docker build -t 이름:태그 . → docker push 내아이디/이름:태그
⑥ 멀티 플랫폼 docker buildx build --platform linux/amd64,linux/arm64 ... --push .
⑦ 보안 일반 사용자(USER)로 실행, 공개 저장소에 비밀 정보 금지
참고 자료
더 깊이 보고 싶다면 (볼트 내 정리 노트):
공식 문서:
- Dockerfile reference: https://docs.docker.com/reference/dockerfile/
- Build best practices: https://docs.docker.com/build/building/best-practices/
- Multi-platform builds: https://docs.docker.com/build/building/multi-platform/